Intro
The aim of this project is to allow any user of Adventure Game Studio to readily include a Battle Ships minigame in their game. Now you know.
I expect that the one of the first two responses two this will be:
This file will – hopefully – walk you through the stages needed to get a game ready for playing Battle Ships.
1) “Looks interesting. I haven’t downloaded it yet.”
This help file has two main chapters. Chapter 1 is a step-by-step walkthrough to help you add the modules to your game and get a standalone battleship game running. Chapter 2 is more detailed look at the individual functions included in the modules and how to use them if you want to try and make a more customised version of the game.
This is written for those who have a reasonable working knowledge of how to use AGS. Such a person has been playing with it for more than 3 days, knows how to use scripting has at least a vague understanding of programming (Or, at the very least, how to SPELL programming) and is happy importing script modules and graphics and such without needing someone to hold their hand. If that’s you, great! Let’s begin.
It would be a good idea to read this through BEFORE you start work. This walkthrough assumes you are starting with a blank canvas having not even prepared your background, sprites and sounds for this game. (Which are kind of mandatory) If you have already done this before you even begin coding the game, then you’ll save yourself a lot of time.
You will need to have AGS V2.72 RC2a or better. The modules will work with AGS V2.71, but I found the task easier with V2.72.
At the moment, the project itself comprises two modules:
BshipVars.scm
Battleships.scm
The first module in itself does very little besides providing an area to input your own variables as necessary for customising the game. It also includes one function to act as ‘idiot-checking’ for the variables.
The actual mechanics of the game are buried in the second module. This includes the rather primitive AI by which the computer selects its next move, along with many useful functions to process the commands as they come through.
When you are including this minigame in your own AGS game, you need to import both modules via the module manager and ensure that the bShipVars module is imported above the main BattleShips module.
Don’t go compiling the game just yet! There’s a long way to go first!
Your first task is to import your background for the game. When drawing out the board, you need to include:
* A grid to contain the enemy’s fleet.
* A grid for the player’s fleet.
* A space for the player’s ships to be stored before he has placed them.
Make sure that each grid is 10 x 10, with all squares within the grid the same size. It’s a simple copy-and-paste job. It’s a good idea to ensure that the border around each field within each grid is one pixel wide – unlike the grids below.
Make the sure that the room is the right size for the screen resolution you’re going for.
|
Go ahead and create a new room in AGS. Import your new background and admire its beauty. Or crapness, depending on how little talent you have for drawing. Save your game and the room. Why not attach a description to it in the room list?
At the moment, you have targets to aim at. But you don’t actually have any ships to sink. Let’s remedy this.
For this game, you need 5 ships:
* A Submarine (Occupying 2 squares)
* A Frigate (Occupying 3 squares)
* A Cruiser (Occupying 3 squares)
* A Battleship (Occupying 4 squares)
* An Aircraft Carrier (Occupying 5 squares)
Each ship lies in a single horizontal row (But it will be rotated if necessary) and comprises n contiguous squares as described above.
The easiest way to ensure that your ships are the right size is to cut out a section of grid from your background and use the required number of squares as a guide to the maximum possible area. At the moment, each ship’s sprite graphic must be exactly the required size, but you can leave the remaining area blank if you wish. (The aim is to make it transparent when you import it)
|
This four-square arrangement… |
|
… gives this Battleship. (The purple colour is intended to be the transparent colour. At least it would if MSPaint hadn’t ‘helpfully’ blurred it all to buggery.) |
Make your ships’ graphics in this way. There is no reason why the Frigate and Cruiser can’t be the same graphic; you may save yourself some time in doing this.
Now import your ship graphics into AGS. It may be an idea to create a separate folder for these. Ensure that when you import, the editor correctly detects the transparent area.
Just like you did for the ship graphics, cut out a single square from a grid. Then colour this in to make a graphic for a shot that’s either a hit or a miss. I won’t draw an image here, because you should be able to do this yourself.
Import these graphics as before, being careful of any attempts to render your graphic transparent.
You need a pointer graphic. This shouldn’t be too difficult to whip up:
Our new mouse pointer. I hope you have more time to make something better than this. |
Import it into the AGS editor and set it up as the image for the “Pointer” mouse mode (Cursor mode 6, methinks) Take a moment to set the cursor’s hotspot to the centre of the image, and make a mental note of the co-ordinates.
The current method of handling the interactions for this minigame requires a minimum of three (3) hotspots to be created. In case you haven’t already guessed what these are for, they are:
* One for the enemy’s grid. (Given the script-o-name ‘enemygrid’)
* One for the player’s grid. (Given the script-o-name ‘mygrid’)
* One for the ‘box’ containing the ships. (Given the script-o-name ‘toolbox’)
Draw a hotspot area covering the grid that the player will take shots at. This is the enemy’s grid. Give it the Script-o-name “enemygrid”. (When you reference it, it will be referred to as “henemygrid”) Now draw another hotspot area covering the grid that the player will place his ships on, and the computer will fire at. Give this the script-o-name “mygrid”. Finally, draw a hotspot area over the box for the player’s ships. Give this the script-o-name “toolbox”.
Obviously, the text labels won’t appear like that when you do this. |
Gasp! It’s time to open the script editor to fiddle blindly with the variables!
Open the AGS Module Manager, and then select the BshipVars module in the listbox. Go ahead and read the description if you like. Then, click on the “Edit Header” button.
|
Take a look at the script that pops up. I’ve added many lines of comments to guide you through what each of the stated variables do, and how you should amend them. You can read through the whole header to get the gist of what’s happening, but for now, we’re only going to edit the first five variables.
Look towards line 81 in the script editor. In particular, you should see the following:
#define BShipVars_ENEMY_FLEET_X_OFFSET 23
#define BShipVars_ENEMY_FLEET_Y_OFFSET 0
#define BShipVars_PLAYER_FLEET_X_OFFSET 160
#define BShipVars_PLAYER_FLEET_Y_OFFSET 99
These lines specify the co-ordinates of the top left corner of the playing grids. When the game is compiled, any occurrence of “BShipVars_ENEMY_FLEET_X_OFFSET” will be replaced with23and so on. You need to replace the numbers at the end of each line with the x and y co-ordinates of the top left corner of the player’s and enemy’s grid. For example, if the top left corner of the enemy’s grid is found at (100, 50) and the top left corner of the player’s grid can be found at (200, 20), then you should amend these lines as follows:
#define BShipVars_ENEMY_FLEET_X_OFFSET 100
#define BShipVars_ENEMY_FLEET_Y_OFFSET 50
#define BShipVars_PLAYER_FLEET_X_OFFSET 200
#define BShipVars_PLAYER_FLEET_Y_OFFSET 20
You also need to look at the next variable on line 97:
#define BShipVars_FIELD_SIZE 10
You need to change this value to the number of pixels along each side of each square in each grid. So, if each square is 15 pixels along each side, then the line should be amended to read:
#define BShipVars_FIELD_SIZE 15
NOTE: If your screen resolution is anything other than 320 x 200, then the size of each square and the locations of the corners will be different from those you may have noticed when editing your background. Use the mouse pointer in the AGS room editor to find the co-ordinates needed.
Now, look for the following at line 111:
#define BShipVars_HIT_SPRITE 11
#define BShipVars_MISS_SPRITE 12
As you might expect, the number after each name is the slot in the sprite manager where you can find each of the hit and miss sprites. Amend the numbers accordingly.
Now look further down to line 128:
#define BShipVars_MOUSE_POINT_SPRITE 5
#define BShipVars_MOUSE_HOT_X 3
#define BShipVars_MOUSE_HOT_Y 3
The first number is the slot of the pointer graphic. The second and third numbers are the x and y co-ordinates of the cursor hotspot. Change the numbers as required.
Finally, look down to line 157:
#define BShipVars_SHIP_SUB_SPRITE 1
#define BShipVars_SHIP_FRIGATE_SPRITE 2
#define BShipVars_SHIP_CRUISER_SPRITE 2
#define BShipVars_SHIP_BATTLESHIP_SPRITE 3
#define BShipVars_SHIP_CARRIER_SPRITE 4
These numbers are the sprite slots of the individual ships. In case you’ve forgotten how long each ship is meant to be, a handy comment above these lines will be most helpful. Amend the numbers as necessary.
Now close the script editor, being sure to save your changes. That is the last time you will edit the Script Header of BshipVars. Well, it will be if you’ve got it right.
Nope. We’re not finished yet. Still more work to do. Here it gets difficult.
In your room, you need to create ten (10) objects. They must be in this order:
Object 0 |
Computer’s SUB |
Object 5 |
Player’s SUB |
Object 1 |
Computer’s FRIGATE |
Object 6 |
Player’s FRIGATE |
Object 2 |
Computer’s CRUISER |
Object 7 |
Player’s CRUISER |
Object 3 |
Computer’s BATTLESHIP |
Object 8 |
Player’s BATTLESHIP |
Object 4 |
Computer’s AIRCRAFT CARRIER |
Object 9 |
Player’s AIRCRAFT CARRIER |
Place the first 5 (0 – 4) in the enemy’s grid, and ensure that the “Object is initially visible” checkbox is unchecked. Place the other five neatly in the toolbox and ensure that their “Object is initially visible” checkbox is checked.
Okay. There’s a lot to do in this section.
Call up the Room’s General Interaction editor (That’s the one that deals with a player entering and leaving the room, not the individual hotspots and regions.)
Under the “Player Enters Room (After FadeIn)” interaction, insert a new “Run Script” action. Now edit this “Run Script” action.
Type the following:
BatShip.initialise(1);
cross_hair_on = false;
This will initialise the game and set out the computer’s ships in his grid. It will also deactivate the cross hair. (Since we haven’t imported the graphics for it yet, if we don’t deactivate it, the game will crash when the computer takes a shot.)
The cross-hair is the only ‘optional’ bit at this point, and can easily be activated later. See the end of this section for more info.
Save this and close the script editor and the interaction editor.
Now, call up the interaction editor for hotspot that covers the Enemy’s Grid (You may recall I use hotspot number 1 for this purpose). Insert a new “Run Script” action under the “Any Click on Hotspot” interaction. Edit this “Run Script” action, and type the following:
if (game_stat == eStatPlayerTurn) {
int x = BatShip.player_click();
if (x == 0) computers_turn();
else if (x == 1) players_turn();
else if (x == 2) game_over();
}
Whenever a player clicks on the enemy’s grid, he will either miss (x will become 0 and the computer will have a go) or he will hit (x will become 1 and the player can have another go) or he will hit the last section of the last ship standing. (x will become two and the game will be over.
Incidentally, the computers_turn(), players_turn() and game_over() functions must be manually added to the room script. This will allow you to customise any effects or changes that happen. We’ll do that now.
Close the script editor and the interaction editor. Now, open up the room’s script file in the script editor and, towards the top of the script, add the following:
function players_turn() {
game_stat = eStatPlayerTurn;
}
function game_over() {
game_stat = eStatGameOver;
}
function computers_turn() {
game_stat = eStatCompTurn;
int x = 1;
while (x == 1 && game_stat == eStatCompTurn) {
x = BatShip.computers_turn();
while (IsSoundPlaying() != 0) {
Wait(1);
}
}
if (game_stat == eStatPlayerTurn) players_turn();
else if (game_stat == eStatGameOver) game_over();
}
This is the BARE MINIMUM you need. If you want to have a status bar detailing whose go it is, you can add extra code to these functions as you wish.
Technical Note: There is some duplication of script here. The script module may assign the necessary values to the variable game_stat, thus making those lines pointless here. But if you leave them in, it will be fine anyway.
While you have the room script editor open, paste in this function:
function on_mouse_click(int button) {
if (button == eMouseRight) {
if (game_stat == eStatPlaceShips && ship_on_cursor != 0) {
BatShip.rotate_ship();
ClaimEvent();
}
}
}
This function is needed to allow you to rotate a ship while it’s on the cursor by pressing the right mouse button.
Now, we have more interaction editing ahead of us.
Open the interaction editor for the hotspot that covers the player’s grid. Under the interaction “Any Click on Hotspot”, add a new “Run Script” action. Edit it in the script editor and add the following:
if (game_stat == eStatPlaceShips && ship_on_cursor != 0) {
if (BatShip.drop_player_ship() == 1) players_turn();
}
Again, this is the minimum required. If you want a message to pop-up during the game warning the player that he shouldn’t be targeting his own ships, you can do that yourself.
Now, open the Interaction editor for the Hotspot covering the toolbox. Create a new “Run Script” action under the “Any click on Hotspot” interaction and add the following line:
BatShip.replace_ship();
Five more left!
The last interactions that need sorting before you can begin are those of your own ships.
For each of the Player’s ships (5 – 9) Insert a “Run Script” action under their “Any Click on Object” interaction. For each, you need to insert this code:
if (game_stat == eStatPlaceShips && ship_on_cursor == 0 && Hotspot.GetAtScreenXY(mouse.x,mouse.y) != hmygrid) {
BatShip.set_mouse_cursor(eCurSub);
}
Mouse.Mode = eModePointer;
The eCurSub is in bold for a very good reason: you need to change this to the enum value corresponding to each ship. The values are as follows:
enum ShipsOnCursor {
eCurNone = 0,
eCurSub = 1,
eCurFrig = 2,
eCurCru = 3,
eCurBat = 4,
eCurCar = 5,
};
So, for the submarine, you leave eCurSub unchanged. For the Frigate, you change it to eCurFrig. For the Cruiser, you change it to eCurCru and so on.
Notice that if you’ve imported the ships in the right order, then the right enum value is equal to the object number minus four (4).
Open the rooms Message Editor, and input the following five (5) messages:
Message # |
What it needs to say |
0 |
<Opponent has sunk one of the player’s ships> |
1 |
<Player has sunk one of the opponent’s ships> |
2 |
<Opponent has won the game> |
3 |
<Player has won the game> |
4 |
<The player is trying to place a ship somewhere impossible> |
And finally, import three (3) sound files:
Sound#.* |
What it’s for |
1 |
A ship has been sunk. |
2 |
A player has missed. |
3 |
A player has scored a hit. |
And that’s it! Save your game and play it through.
If you’ve just thrown some silly graphics together then you may realise what a good idea it may be to have another go to make it better. You may even have to start again to do so, but at least you know what’s needed now, right?
In the demo game, you’ll notice that when the computer took a shot, a big red target aimed at the square in question before it was blown up. This is what you need for it.
You need one graphic that is a thin horizontal line, and whose length is equal to the width of the game resolution. (If the game is 640 x 480, then it needs to be 640 pixels long in your paint program)
Next, you need one graphic that is a thin vertical line, and whose height is equal to the height of the game resolution. (If the game is 800 x 600, it needs to be 600 pixels high in the paint program you’re using)
Import these two graphics into the editor – being careful that they don’t get made transparent – and then go back to the header of the BshipVars Module. Look for the following lines at line 176:
#define BShipVars_CROSS_HAIR_HORIZ 14
#define BShipVars_CROSS_HAIR_VERT 15
Change the numbers at the end to the sprite slots of the horizontal and vertical lines. Finally, go back to the room script where you entered the line to cancel the cross-hair. Change the false to a true.
Part II: A look at the individual functions in the modules.
These functions are contained within a struct called ‘BatShipVars’ that is imported from the Header of this module. If you type “BatShipVars.” The autocomplete should do it’s job for you.
function check_defines()
The only function contained within the BshipVars module itself is the one function that runs through each of the #defines and ensures that they aren’t clearly wrong. If one is found to be erroneous, the game will be aborted using the AGS ‘AbortGame()’ command. Here are the conditions which, at present, cause the game to abort:
#define name |
Condition that causes an abort |
BShipVars_ENEMY_FLEET_X_OFFSET BShipVars_PLAYER_FLEET_X_OFFSET |
If it is less than 0, or greater than (320 – (10 x FIELD_SIZE) ) |
BShipVars_ENEMY_FLEET_Y_OFFSET BShipVars_PLAYER_FLEET_Y_OFFSET |
If it is less than 0, or greater than (200 – (10 x FIELD_SIZE) ) |
BShipVars_FIELD_SIZE |
If it is less than 1 or greater than 20. |
BShipVars_HIT_SPRITE BShipVars_MISS_SPRITE
BShipVars_MOUSE_POINT_SPRITE
BShipVars_SHIP_SUB_SPRITE BShipVars_SHIP_FRIGATE_SPRITE BShipVars_SHIP_CRUISER_SPRITE BShipVars_SHIP_BATTLESHIP_SPRITE BShipVars_SHIP_CARRIER_SPRITE
BShipVars_CROSS_HAIR_HORIZ BShipVars_CROSS_HAIR_VERT |
If it is less than 1. |
BShipVars_MOUSE_HOT_X |
If it is less than 0, or greater than the width of the pointer sprite. |
BShipVars_MOUSE_HOT_Y |
If it is less than 0, or greater than the height of the pointer sprite. |
NOTE: This list is not exhaustive. There are – probably – still many ways you can cause unusual or wrong results by entering erroneous values that this function doesn’t catch.
Most of these functions are declared within a struct. An instance of this struct called ‘BatShip’ is exported from the header of this module. Typing “BatShip.” will let the autocomplete do the work for you.
function randomise()
This function counts the number of seconds elapsed since midnight and uses this to randomise the pseudorandom number generator that this project uses. Ordinarily, it isn’t necessary. However, if you restart the game, then you may notice that the random number generator repeats itself exactly.
function coord_write(int x_coord, int y_coord)
This function can be called at any time, but it will only produce anything at all if the game is being run in AGS Debug Mode. This will simply take whatever values of x_coord and y_coord and append them to a plain text file in the game’s Compiled directory. You can use to assess how the computer is choosing its moves and whether there are any patterns to its decisions. Useful for trying to find bugs in the AI code.
function cross_hair_target(int x_coord, int y_coord)
If you’ve played through the demo game, you’ll have doubtlessly noticed the red crosshair that targets squares on the player’s grid. This is the function that does this.
If you do not want the crosshair in your game, then you’ll need to remove the line that calls this function from the top of the computer_attack function. (See later)
function BattleShips::all_deployed()
After a ship is dropped onto the player’s grid, this function is called to check whether all the ships have been deployed. If they have, this function returns 1. Otherwise, it returns 0.
static function BattleShips::set_mouse_cursor(int ship)
When this function is called, a parameter must be passed that identifies the ship that will be placed on the mouse cursor. The mouse hotspot is altered to being flat against the left hand side of the sprite and midway between the top and bottom of the sprite. This function is typically called when the player has picked up a ship and is looking for somewhere on the grid to place it.
There is a special enumerated type for the identity of the ship that you wish to place on the cursor:
enum ShipsOnCursor {
eCurNone = 0,
eCurSub = 1,
eCurFrig = 2,
eCurCru = 3,
eCurBat = 4,
eCurCar = 5,
};
function BattleShips::reset_mouse_cursor()
This function simply changes the mouse cursor graphic back to the default mouse pointer sprite, and resets the hotspot.
static function BattleShips::mouse_x_to_x_coord(int mouse_x, int the_player)
static function BattleShips::mouse_y_to_y_coord(int mouse_y, int the_player)
These functions convert the mouse’s current x and y coordinates to the x and y coordinates on the grid of either the player or the computer. If you want to know how this is done, then look at the functions themselves.
The the_player value is either 0 or 1. There is a helpful enum type for this purpose:
enum WhosGoIsIt {
eGoPlayer = 1,
eGoComputer = 0,
};
If you are targeting the computer’s grid, then pass eGoComputer or 0. If you are placing ships on the player’s grid, pass eGoPlayer or 1.
static function BattleShips::Set_Tile (int who, int x, int y, int value)
Sets the value of what ever is at the (x, y) co-ordinate of the either the player’s or the computer’s grid to be value.
The who parameter is either 0 or 1, but you can use the WhosGoIsIt enum as described earlier instead.
If either of x or y are greater than 9 or less than 0, the function terminates and returns the value 666.
static function BattleShips::Get_Tile (int who, int x, int y)
Returns the value of whatever is at the (x, y) co-ordinate of either the player’s or the computer’s grid.
The who parameter can be either 0 or 1, but you can use the WhosGoIsIt enum as described earlier instead.
If either of x or y are greater than 9 or less than 0, the function terminates and returns 666.
static function BattleShips::drop_player_ship()
This should be called when the player has clicked on his own grid, and has a ship attached to his cursor. If the ship can be dropped into it’s location, it will be done.
This function will return 1 if the drop was successful AND, as a result, all ships have been deployed, following a call to the all_deployed function.
static function BattleShips::player_click()
This function is used to process a click made on the computer’s grid. It checks to see if there is a ship on that square. If so, that portion of the ship is blown up, the HIT_SPRITE is painted underneath it and the explosion sound is played. The function then returns 1. If the explosion completely destroys a ship, a message is displayed. If the explosion destroys the last ship standing, then the function will display a second message and return 2 instead. You can use this to determine if the game is over.
If there is nothing there, the water is blown up, the MISS_SPRITE is painted there and a dull sound is played. The function then returns 0.
static function BattleShips::rotate_ship()
If the ship is horizontal, it will become vertical. If the ship is vertical, it will be made horizontal.
What? Did you expect it to sing and dance as well?
static function BattleShips::replace_ship()
If the player, while having a ship attached to his cursor, clicks inside the toolbox containing the ships, then this function can be called to remove the ship from the cursor and put it back in the box.
static function BattleShips::computer_attack()
This is the computer’s equivalent of the players_click() function described earlier. It returns 0 if a miss occurs, and a 1 if there was a hit.
In addition, if the computer shoots out the last remaining section of the last of the player’s ships still standing, then it reveals the locations of its ships on its grid. This is done by making them semi-transparent. So, if your game is a 256 colour one, be aware that this section will, at present, cause problems.
static function BattleShips::computers_turn()
WARNING: THIS FUNCTION IS VERY BADLY CODED
This function is the very complicated AI that, at present, only half works. Edit at your own risk!
By and large, the function is meant to follow a simple enough premise:
1) Chose the next target square. Choose it randomly to begin with
2) Fire at it (Using the computer_attack function)
3) Was it a hit (Go to 4) or a miss? (Go back to 1)
4) Excellent! Use that field as a starting point and find the rest of the ship.
5) Choose the next target square.
Unless the computer has already hit a ship, it will “Comb” the grid in the following manner:
x-x-x-x-x-
-x-x-x-x-x
x-x-x-x-x-
-x-x-x-x-x
x-x-x-x-x-
and so on.
This function uses the comp_fire_pattern enum type:
enum comp_fire_pattern {
eCompFireComb = 0,
eCompFireNxtNESW = 1,
eCompFireDestroyShip = 2,
eCompFireDestroyNext = 3,
};
At present, only the first two options are used at all in the function.
This function will return 0 if it failed to hit a ship section on that go.
The function will return 1 if it did indeed hit a section of the ship. BUT it will also return 1 if it realises it will get stuck in a loop (Start looking at line 431) this is done to stop the program hanging if it picks out a square containing a ship with no immediate neighbours.
Calling this function from a loop as follows will allow the computer to follow the rule that “If you score a hit, you can have another go”:
int x = 1;
while (x == 1 && game_stat == eStatCompTurn) {
x = BatShip.computers_turn();
while (IsSoundPlaying() != 0) {
Wait(1);
}
}
Incidentally, that inner while loop is to force the computer to pause until the sounds have finished playing. Otherwise, you may see the crosshair of doom sweep very quickly across your battleship. If found it unsettling, myself.
static function BattleShips::place_ship(int ship)
This function is analogous to the drop_player_ship() function described above. It will drop a given ship onto the computer’s grid.
The ship parameter can be from 1 to 5, or you can use the following enum:
enum Ships {
eShipEmpty = 0,
eShipSub = 1,
eShipFrigate = 2,
eShipCruiser = 3,
eShipBattle = 4,
eShipCarrier = 5,
eShipWreck = 6,
};
The ship will be placed on the grid and made invisible. The areas of the grid that are occupied by the ship will be updated accordingly.
static function BattleShips::initialise(optional int place_ships)
Last one!
Call this function when you start the game, and everything will be initialised:
* The grids will be wiped clean.
* The #defines in the BshipVars module will be checked.
* The hit points for each ship will be reset.
* The ship sprites will be stored into memory.
* The dimensions of the grids will be stored into memory.
* If the game.debug_mode is active, then the co-ords file will be prepped.
* The randomise function will be called.
* The status of the game will be set to “Place Ships”
* The mouse cursor mode will be set to the Pointer mode.
In addition, if you pass 1 as the parameter, the computer’s ships will be automatically placed. You can omit this if you want to place the ships yourself later on.
Well, I hope this documentation has been useful to you. There’s plenty of scope for further improvements to this module:
* Improve the AI.
* ‘Standardise’ the module.
* DynamicSprites are not deleted before the game is finished.
* Want different sized ships? It’s just a few edits away!
* Different rules? Possible, just not at the moment.
* Different ships for each side? It’s Do-able.
* Maybe, sometime, you’ll be able to have oddly-shaped ships. (Think about the “Battle Cruiser” game from Space Quest V.)
And many other possibilities.
V 0.1 – First version 02 / August / 2006